package com.jf.cloud.coupon.manager;

import com.jf.cloud.common.cache.constant.CacheNames;
import com.jf.cloud.common.cache.constant.CouponCacheNames;
import com.jf.cloud.common.cache.constant.LockCacheNames;
import com.jf.cloud.common.cache.util.RedisUtil;
import com.jf.cloud.common.constant.Constant;
import com.jf.cloud.common.util.Arith;
import com.jf.cloud.coupon.constant.CouponConstant;
import com.jf.cloud.coupon.constant.CouponRedisLua;
import com.jf.cloud.coupon.mapper.CouponMapper;
import com.jf.cloud.coupon.mapper.CouponUserMapper;
import com.jf.cloud.coupon.model.Coupon;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.util.Objects;
import java.util.Random;

@Service
public class CouponStockManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(CouponStockManager.class);

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private CouponUserMapper couponUserMapper;

    public Integer getAvailableZone(Long couponId) {
        Integer availableZone = RedisUtil.get(CouponCacheNames.COUPON_STOCK_AVAILABLE_ZONE + couponId);
        if (Objects.nonNull(availableZone)) {
            LOGGER.info("优惠券{}可用分片{}", couponId, availableZone);
            return availableZone;
        }
        // 缓存被删除/未创建
        Integer stock = getStockByCouponId(couponId);
        return doZone(couponId, stock);
    }

    /**
     * 获取正在投放的优惠券的真实库存
     */
    public Integer getStockByCouponId(Long couponId) {
        int totalStock = 0;
        RLock lock = redissonClient.getLock(LockCacheNames.LOCK_COUPON_ZONE_PERFIX + couponId);
        lock.lock();
        try {
            // 遍历redis查找优惠券库存
            for (int i = 0; i < CouponConstant.ZONE_NUM; i++) {
                Integer zoneStock = RedisUtil.get(CouponCacheNames.COUPON_STOCK + couponId + CacheNames.UNION_KEY + i);
                if (Objects.isNull(zoneStock)) {
                    LOGGER.info("优惠券{}分片库存缓存不存在", couponId);
                    // 缓存查不到，可能是缓存被删除/未创建
                    // 通过数据库获取库存数量，这里的库存数量不一定准确的
                    Coupon dbCoupon = couponMapper.getByCouponId(couponId);
                    // 总库存量，需要查一下领取数量，才知道到底还有多少库存
                    int receiveCouponNum = couponUserMapper.countReceiveCoupon(couponId);
                    // 剩余库存
                    int stockByReceive = dbCoupon.getTotalStock() - receiveCouponNum;
                    // 看下通过领取数出来的库存，和原始库存那个比较少，那个少哪个就最接近“真实库存”。
                    // 所谓的接近真实都是建立在缓存被删了，然后没办法恢复的状态，只能通过领取数量和历史库存进行查询的结果。
                    // 正常情况下，缓存的库存就是真实库存。
                    return Math.min(dbCoupon.getStocks(), stockByReceive);
                } else {
                    totalStock += zoneStock;
                }
            }
        } finally {
            lock.unlock();
        }
        return totalStock;
    }

    public Integer doZone(Long couponId, Integer totalStock) {
        // 分片数量
        int zoneNum = CouponConstant.ZONE_NUM;
        // 可用分片数量
        int availableZone = 0;
        if (totalStock == 0) {
            LOGGER.info("优惠券{}库存为0，分片还原", couponId);
            RedisUtil.set(CouponCacheNames.COUPON_STOCK_AVAILABLE_ZONE + couponId, availableZone, 0L);
            // 分片库存数量归零，所以可以把缓存删了，并且库存落库，可用分片数量的缓存可以继续留用
            flushStockToDb(couponId,0);
            for (int i = 0; i < zoneNum; i++) {
                RedisUtil.del(CouponCacheNames.COUPON_STOCK + couponId + CacheNames.UNION_KEY + i);
            }
            return availableZone;
        }

        // 每个分片至少的库存数量（目前是想分片出来的结果要么是这个数量，要么是0）
        // 比如 ( 10,10,5,0,0,0,0,) 这种结果
        int avgStock = getAvgStock(zoneNum, totalStock);
        // 获取要存进缓存的每个分片库存数量
        for (int i = 0; i < zoneNum; i++) {
            if (totalStock != 0) {
                availableZone = i + 1;
            }
            // 更新缓存
            if (totalStock - avgStock >= 0) {
                RedisUtil.set(CouponCacheNames.COUPON_STOCK  + couponId + CacheNames.UNION_KEY + i, avgStock, 0L);
                totalStock -= avgStock;
            } else {
                RedisUtil.set(CouponCacheNames.COUPON_STOCK  + couponId + CacheNames.UNION_KEY + i, totalStock, 0L);
                totalStock = 0;
            }
        }
        // 缓存可用分片数量
        RedisUtil.set(CouponCacheNames.COUPON_STOCK_AVAILABLE_ZONE + couponId, availableZone, 0L);
        return availableZone;
    }

    private static int getAvgStock(int zoneNum, Integer totalStock) {
        // 每个分片可以分配的库存数量，可能小于10（这里的平均库存并不是真正的平均库存，因为这里进行了线上取整）
        // 比如平均库存是 333.33 可以能会变成334
        int everyZoneStockRoundUp = Arith.divToRoundUp(totalStock, zoneNum);

        // 平均库存向上取整，因为就算是平均库存
        return Math.max(everyZoneStockRoundUp, CouponConstant.ZONE_MIN_STOCK);
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean reduceStockByZone(Long couponId, Integer stock, int availableZone) {
        // 先对随机单个分片进行扣减
        Random random = new SecureRandom();
        int zoneNo = random.nextInt(availableZone);

        Long hasStock = RedisUtil.eval(CouponRedisLua.REDUCE_STOCK_MUST_ENOUGH_SCRIPT,
                CouponCacheNames.COUPON_STOCK + couponId + CacheNames.UNION_KEY + zoneNo, stock.toString(), Long.class);
        // 库存每剩余100件就更新一遍库存
        if (hasStock % Constant.HUNDRED == 0L) {
            LOGGER.info("优惠券{}更新库存到数据库", couponId);
            // flush to db
            flushStockToDb(couponId,null);
        }

        if (hasStock == 0L) {
            LOGGER.info("优惠券{}可用分片数量-1", couponId);
            // 可用库存分片数量减一
            RedisUtil.decr(CouponCacheNames.COUPON_STOCK_AVAILABLE_ZONE + couponId, 1L);
        }
        return hasStock >= 0L;
    }

    @Transactional(rollbackFor = Exception.class)
    public void flushStockToDb(Long couponId, Integer hasStock) {
        // 可用库存
        if (Objects.isNull(hasStock)) {
            hasStock = getStockByCouponId(couponId);
        }
        Coupon dbCoupon = couponMapper.getByCouponId(couponId);
        // 数据库的可用库存
        Integer dbStocks = dbCoupon.getStocks();
        // 改变的库存
        int changeStock = dbStocks - hasStock;
        if (changeStock == 0) {
            LOGGER.info("优惠券{}库存未改变，无需更新", couponId);
            return;
        }
        // 更新剩余库存
        couponMapper.updateStockById(couponId, changeStock);
    }
}
